Pythonデコレーター 2:引数を持つデコレーター
この資料を執筆時点で、彼の記事は13年前のものになりますが、未だに有益な資料です。
Python コードについては、Python3 で動作するように修正しています。
著者: Bruce Eckel
2008年10月19日
概要
デコレーターの仕組みは、デコレーターに引数を渡すとかなり違った動作をします。
復習:引数のないデコレータ
パート1では、引数のないデコレータの使い方を紹介しました。主にクラスをデコレータとして使用しましたが、それはクラスの方が考えやすいからです。
引数のないデコレータを作成すると、 デコレーションされる関数がコンストラクタに渡され、 デコレーションされた関数が呼び出されるたびに __call__() メソッドが呼び出されます。
code: python
class decoratorWithoutArguments(object):
def __init__(self, f):
"""
デコレーターの引数がない場合は、
デコレートされる関数がコンストラクターに渡されます。
"""
print("Inside __init__()")
self.f = f
def __call__(self, *args):
"""
デコレートされた関数が呼び出されるまで、
__call__メソッドは呼び出されません。
"""
print("Inside __call__()")
self.f(*args)
print("After self.f(*args)")
@decoratorWithoutArguments
def sayHello(a1, a2, a3, a4):
print('sayHello arguments:', a1, a2, a3, a4)
print("After decoration")
print("Preparing to call sayHello()")
sayHello("say", "hello", "argument", "list")
print("After first sayHello() call")
sayHello("a", "different", "set of", "arguments")
print("After second sayHello() call")
デコレートされた関数の引数はすべて __call__() に渡されます。実行すると次の出力となります。
code: bash
Inside __init__()
After decoration
Preparing to call sayHello()
Inside __call__()
sayHello arguments: say hello argument list
After self.f(*args)
After first sayHello() call
Inside __call__()
sayHello arguments: a different set of arguments
After self.f(*args)
After second sayHello() call
__init__() はデコレートを行うために呼び出される唯一のメソッドであり、__call__() はデコレートされたsayHello()を呼び出すたびに呼び出されることに注意してください。
引数を持つデコレータ
さて、上の例を修正して、デコレータに引数を追加したときに何が起こるかを見てみましょう。
code: python
class decoratorWithArguments(object):
def __init__(self, arg1, arg2, arg3):
"""
デコレーターに引数がある場合は、
デコレートされる関数はコンストラクターに渡されません
"""
print("Inside __init__()")
self.arg1 = arg1
self.arg2 = arg2
self.arg3 = arg3
def __call__(self, f):
"""
デコレーターの引数がある場合、
__call__()はデコレート処理の一環として一度だけ呼ばれます!
この関数には、関数オブジェクトという1つの引数しか与えられません。
"""
print("Inside __call__()")
def wrapped_f(*args):
print("Inside wrapped_f()")
print("Decorator arguments:", self.arg1, self.arg2, self.arg3)
f(*args)
print("After f(*args)")
return wrapped_f
@decoratorWithArguments("hello", "world", 42)
def sayHello(a1, a2, a3, a4):
print('sayHello arguments:', a1, a2, a3, a4)
print("After decoration")
print("Preparing to call sayHello()")
sayHello("say", "hello", "argument", "list")
print("after first sayHello() call")
sayHello("a", "different", "set of", "arguments")
print("after second sayHello() call")
出力を見ると、動作がかなり大きく変化していることがわかります。
code: bash
Inside __init__()
Inside __call__()
After decoration
Preparing to call sayHello()
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: say hello argument list
After f(*args)
after first sayHello() call
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: a different set of arguments
After f(*args)
after second sayHello() call
デコレートのプロセスでは、コンストラクタが呼び出された後、すぐに __call__() が呼び出されます。この __call__() は、単一の引数 (関数オブジェクト) しか取らず、元の関数を置き換えるデコレートされた関数オブジェクトを返さなければなりません。__call__() はデコレート中に一度だけ呼び出され、その後は __call__() から返されたデコレートされた関数が実際の呼び出しに使用されることに注意してください。
コンストラクタはデコレータの引数を取得するために使用されますが、 __call__() はデコレートされた関数の呼び出しとしては使用できないので、 代わりに __call__() を使用してデコレーションを実行しなければなりません。それでも、初めて見たときは驚きます。
デコレーター引数を持つデコレーター関数
最後に、もっと複雑なデコレータ関数の実装を見てみましょう。ここでは、すべてを一度に行わなければなりません。
code: python
def decoratorFunctionWithArguments(arg1, arg2, arg3):
def wrap(f):
print("Inside wrap()")
def wrapped_f(*args):
print("Inside wrapped_f()")
print("Decorator arguments:", arg1, arg2, arg3)
f(*args)
print("After f(*args)")
return wrapped_f
return wrap
@decoratorFunctionWithArguments("hello", "world", 42)
def sayHello(a1, a2, a3, a4):
print('sayHello arguments:', a1, a2, a3, a4)
print("After decoration")
print("Preparing to call sayHello()")
sayHello("say", "hello", "argument", "list")
print("after first sayHello() call")
sayHello("a", "different", "set of", "arguments")
print("after second sayHello() call")
実行すると、次の出力になります。
code: bash
Inside wrap()
After decoration
Preparing to call sayHello()
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: say hello argument list
After f(*args)
after first sayHello() call
Inside wrapped_f()
Decorator arguments: hello world 42
sayHello arguments: a different set of arguments
After f(*args)
after second sayHello() call
デコレーター関数の戻り値は、デコレートされる関数をラップするための関数でなければなりません。つまり、Pythonは返された関数を受け取り、デコレートされるべき関数を渡してデコレート時にそれを呼び出します。これが3段階の関数がある理由で、内側の関数が実際の置き換え関数となります。
クロージャがあるので、wrapped_f() はクラス版のように明示的に格納しなくても、デコレート引数 arg1、arg2、arg3にアクセスできます。しかし、これは私が「暗黙よりも明示が良い」と考えるケースで、関数バージョンがより簡潔であるにもかかわらず、私はクラスバージョンの方が理解しやすく、したがって修正やメンテナンスもしやすいと考えています。
次回
次回は、デコレーターの実用的な例として、Pythonで作られたビルドシステムを紹介し、最終回では、クラスデコレーターについて見ていきます。
訳注: コメント(Talk Back!) と RSS Feed のセクションは割愛しています。
ブロガーについて
https://gyazo.com/b61197ffc968b0ec1e14d040e2fefb74
Bruce Eckel (www.BruceEckel.com) は、Pythonでの開発支援とFlexでのユーザーインターフェースの提供を行っている。Thinking in Java (Prentice-Hall, 1998, 2nd edition, 2000, 3rd edition, 2003, 4th edition, 2005)、Hands-On Java Seminar CD ROM (Webサイトで入手可能)、Thinking in C++ (PH 1995; 2nd edition 2000, Volume 2 with Chuck Allison, 2003)、C++ Inside & Out (Osborne/McGraw-Hill 1993)などの著書があります。また、世界中で何百ものプレゼンテーションを行い、数多くの雑誌に150以上の記事を掲載しているほか、ANSI/ISO C++委員会の創設メンバーでもあり、カンファレンスでも定期的に講演を行っています。